<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Title</title>
    </head>

    <body>
        <button onclick="routerHistory.push('/')">首页</button>
        <button onclick="routerHistory.push('/about')">关于</button>
        <button onclick="routerHistory.replace('/contact')">联系我们</button>
        <script>
            // 前端路由的实现原理：两种模式一种是hash模式一种history模式
            // window.location.hash = '/';  history.pushState(state,null,url); history.replaceState();
            // 目前浏览器都支持了history.pushState history.pushstate(state,null,url)  history.pushstate(state,null,'#/')

            // 两种路由的区别：
            // hash：hash模式的好处就是锚点，刷新页面的时候不会像服务器发送请求，同时他不支持服务端渲染（不能做seo优化）。不会产生404 忒丑
            // history：特点就是路径漂亮,没有#和正常页面切换一样，如果刷新页面会像服务器发送请求，如果资源不存在会出现 404，解决方案，渲染首页，首页会根据路径重新跳转

            /**
             * 构建路由状态对象
             * @param {string|null} back - 上一个路由地址
             * @param {string} current - 当前路由地址
             * @param {string|null} forward - 下一个路由地址
             * @param {boolean} [replace=false] - 是否使用replace替换当前记录
             * @param {boolean} [computedScroll=false] - 是否记录滚动条位置
             * @returns {object} 路由状态对象
             */
            function buildState(back, current, forward, replace = false, computedScroll = false) {
                return {
                    back,
                    current,
                    forward,
                    replace,
                    scroll: computedScroll ? { left: window.pageXOffset, top: window.pageYOffset } : null,
                    position: window.history.length - 1,
                };
            }

            /**
             * 根据基路径（base）获取当前的完整路由路径
             * @param {string} base - 路由的基路径，可以包含hash（#）模式
             * @returns {string} 当前路由位置（url路径+参数+hash）
             */
            function createCurrentLocation(base) {
                const { pathname, search, hash } = window.location;
                const hasPos = base.indexOf("#");
                if (hasPos > -1) {
                    return base.slice(1) || "/";
                }
                return pathname + search + hash;
            }

            /**
             * 根据传入的基路径，创建并返回history路由的所有API
             * @param {string} base - 路由的基路径，比如'/'或者带#的hash路径
             * @returns {object} - 含有push与replace等方法的history路由对象
             */
            function useHistoryStateNavigation(base) {
                const currentLocation = {
                    value: createCurrentLocation(base),
                };
                const historyState = {
                    value: window.history.state,
                };
                // 第一次刷新页面此时没有任何状态，那么我就自己维护一个状态(页面后退后是哪个路径、当前路径是哪个、要去哪里、使用的是push跳转还是replace跳转、跳转后滚动条位置)
                if (!historyState.value) {
                    changeLocation(currentLocation.value, buildState(null, currentLocation.value, null, true), true);
                }

                /**
                 * 将自定义的状态（state）同步到浏览器原生的 history 路由系统中
                 * @param {string} to - 路由跳转的目标路径
                 * @param {object} state - 路由状态对象
                 * @param {boolean} replace - 是否使用 replaceState 替换当前记录（true）还是 pushState 新增记录（false）
                 */
                function changeLocation(to, state, replace) {
                    const hasPos = base.indexOf("#");
                    const url = hasPos > -1 ? base + to : to;
                    // history原生的api，pushState和replaceState 第2个参数是毫无意义的，所以给个null
                    window.history[replace ? "replaceState" : "pushState"](state, null, url);
                    historyState.value = state;
                }

                /**
                 * 跳转到新的路由地址，或替换当前历史记录的状态
                 * @param {string} to - 跳转的目标地址
                 * @param {object} state - 自定义的 state 状态对象
                 * @param {boolean} replace - 为 true 时使用 replaceState，否则使用 pushState
                 */
                function push(to, data) {
                    // 跳转的时候需要做2个状态，一个是跳转前，从哪去哪
                    const currentState = Object.assign(
                        {},
                        historyState.value,
                        {
                            forward: to,
                            scroll: {
                                left: window.pageXOffset,
                                top: window.pageYOffset,
                            },
                        } // 在当前状态的基础上修改了 forward 和 scroll
                    );
                    // 本质是没有跳转的，只是更新了状态，后续再vue中可以详细监控到状态的变化(这里只是为了修改状态，并未真正跳转，故使用的是replace)
                    changeLocation(currentState.current, currentState, true);

                    const state = Object.assign({}, buildState(currentLocation.value, to, null), { position: currentState.position + 1 }, data);
                    changeLocation(to, state, false); // 真正的更改路径
                    currentLocation.value = to;
                    // 跳转后，从这到了那
                }

                /**
                 * 使用 replaceState 修改当前路由状态，并切换当前路径到指定目标地址
                 * @param {string} to - 替换到的新地址
                 * @param {object} data - 额外需要合并到路由状态中的自定义数据
                 */
                function replace(to, data) {
                    const state = Object.assign({}, buildState(historyState.value.back, to, historyState.value.forward, true), data);
                    // 同步底层真实路由系统
                    changeLocation(to, state, true);
                    // 替换后需要将路径变为现在的路径
                    currentLocation.value = to;
                }

                return {
                    location: currentLocation,
                    state: historyState,
                    push,
                    replace,
                };
            }

            /**
             * 监听浏览器的前进和后退操作，更新当前路由状态和路径，并通知所有注册的监听器
             * @param {string} base - 基础路径
             * @param {object} historyState - 当前历史记录状态的响应式对象
             * @param {object} currentLocation - 当前路径的响应式对象
             * @returns {object} 包含 listener 方法用于外部注册回调
             */
            function useHistoryListeners(base, historyState, currentLocation) {
                const listeners = [];

                function popStateHandler({ state }) {
                    const to = createCurrentLocation(base); // 去哪儿
                    const from = currentLocation.value; // 从哪儿来
                    const fromState = historyState.value; // 从哪儿来的状态
                    currentLocation.value = to;
                    historyState.value = state;
                    const isBack = state.position - fromState.position < 0;

                    listeners.forEach((listener) => {
                        listener(to, from, { isBack });
                    });
                }

                window.addEventListener("popstate", popStateHandler);

                /**
                 * 用于注册路由变化的回调函数，将回调加入监听器数组中。暴露给外部给用户使用
                 * 当浏览器前进或后退时，所有注册的回调会被依次调用。
                 * @param {function} cb - 路由变化时触发的回调，参数为(to, from, { isBack })
                 */
                function listener(cb) {
                    listeners.push(cb);
                }

                return {
                    listener,
                };
            }

            /**
             * 创建一个 HTML5 history 路由对象，管理当前路径和状态，并提供方法进行前进、后退与监听
             * @param {string} base - 路由的基础路径，默认值为空字符串
             * @returns {object} 路由历史对象，包含 location, state, push, replace, listener 等属性和方法
             * 
             * - location: 当前的路由路径（只读）
             * - state: 当前的历史记录状态（只读）
             * - push(location, data): 跳转到新的路由并添加历史记录
             * - replace(location, data): 替换当前历史记录为新的路由
             * - listener(cb): 注册路由变化的回调函数
             * 
             * 该函数整合了 history 的跳转能力以及路由变化监听能力，并暴露简洁的接口供外部使用。
             */
            function createWebHistory(base = "") {
                // 1.路由系统最基本的得包含当前的路径，当前路径下他的状态是什么，需要提供两个场换路径的方法 push replace
                const historyNavigation = useHistoryStateNavigation(base);

                // 2.监听浏览器的前进后退
                const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location);

                // 合并导出
                const routerHistory = Object.assign({}, historyNavigation, historyListeners);

                // 做一个快捷访问方式，省去每次都要写.value
                Object.defineProperty(routerHistory, "location", {
                    get() {
                        return historyNavigation.location.value;
                    },
                });
                Object.defineProperty(routerHistory, "state", {
                    get() {
                        return historyNavigation.state.value;
                    },
                });

                return routerHistory;
            }

            /**
             * 创建一个 hash 模式的路由历史对象
             * 
             * hash 模式下，URL 的路径信息存储在 # 后面，适用于不支持 HTML5 history API 的场景。
             * 
             * - currentLocation.value 的路径会自动去掉 #，方便内部统一处理；
             * - changeLocation 跳转时会自动在路径前补上 #，控制浏览器 hash 的更新；
             *
             * @returns {object} 路由历史对象，与 createWebHistory 接口兼容
             */
            function createWebHashHistory() {
                // hash模式下本质上是做2个事情，一是currentLocation.value中的路径要去掉#，二是changeLocation跳转时要把#补回来
                return createWebHistory("#");
            }

            const routerHistory = createWebHashHistory();

            // const routerHistory = createWebHistory();

            // 实现路由监听，如果路径变化需要通知用户
            routerHistory.listener((to, from, { isBack }) => {
                console.log("to: ", to);
                console.log("from: ", from);
                console.log("isBack: ", isBack);
            });
        </script>
    </body>
</html>
